/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.camera.camera2.internal

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraManager.AvailabilityCallback
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.camera.camera2.AsyncCameraDevice
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat
import androidx.camera.camera2.internal.compat.CameraManagerCompat
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraUnavailableException
import androidx.camera.core.Logger
import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraStateRegistry
import androidx.camera.core.impl.Observable
import androidx.camera.core.impl.utils.MainThreadAsyncHandler
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.impl.fakes.FakeCameraCoordinator
import androidx.core.os.HandlerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.math.ceil
import org.hamcrest.CoreMatchers.equalTo
import org.junit.After
import org.junit.AfterClass
import org.junit.Assume
import org.junit.Assume.assumeThat
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock

/** Contains [Camera2CameraImpl] tests for reopening the camera with failures.  */
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 21)
class Camera2CameraImplCameraReopenTest {
    @get:Rule
    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
        PreTestCameraIdList(Camera2Config.defaultConfig())
    )
    private lateinit var cameraCoordinator: CameraCoordinator
    private var camera2CameraImpl: Camera2CameraImpl? = null
    private var cameraId: String? = null
    private var anotherCameraDevice: AsyncCameraDevice? = null

    @Before
    fun setUp() {
        cameraCoordinator = FakeCameraCoordinator()
        cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)
        Assume.assumeFalse("Device doesn't have an available back facing camera", cameraId == null)
    }

    @After
    @Throws(InterruptedException::class, ExecutionException::class)
    fun testTeardown() {
        // Release camera, otherwise the CameraDevice is not closed, which can cause problems that
        // interfere with other tests.
        if (camera2CameraImpl != null) {
            camera2CameraImpl!!.release().get(5, TimeUnit.SECONDS)
            camera2CameraImpl = null
        }

        anotherCameraDevice?.closeAsync()
        anotherCameraDevice = null
    }

    @Test
    @Throws(Exception::class)
    fun openCameraAfterMultipleFailures_whenCameraReopenLimitNotReached() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Try opening the camera. This will fail and trigger reopening attempts
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Wait for half the max reopen attempts to occur
        val maxReopenAttempts = ceil(REOPEN_LIMIT_MS / REOPEN_DELAY_MS.toDouble()).toInt()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                maxReopenAttempts / 2, REOPEN_LIMIT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Allow camera opening to succeed
        cameraManagerImpl.shouldFailCameraOpen = false

        // Verify the camera opens
        awaitCameraOpen()
    }

    @Test
    @Throws(Exception::class)
    fun doNotAttemptCameraReopen_whenCameraReopenLimitReached() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Try opening the camera. This will fail and trigger reopening attempts
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Wait for max reopen attempts to occur
        awaitCameraMaxReopenAttemptsReached(cameraOpenSemaphore)

        // Verify 0 camera reopen attempts occurred.
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isFalse()
    }

    @Test
    @Throws(Exception::class)
    fun openCameraAfterReopenLimitReached_whenCameraExplicitlyOpened() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Try opening the camera. This will fail and trigger reopening attempts
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Wait for max reopen attempts to occur
        awaitCameraMaxReopenAttemptsReached(cameraOpenSemaphore)

        // Allow camera opening to succeed
        cameraManagerImpl.shouldFailCameraOpen = false

        // Try opening the camera. This should succeed
        camera2CameraImpl!!.open()

        // Verify the camera opens
        awaitCameraOpen()
    }

    @Test
    @Throws(Exception::class)
    fun openCameraAfterReopenLimitReached_whenCameraBecomesAvailable() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Try opening the camera. This will fail and trigger reopening attempts
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Wait for max reopen attempts to occur
        awaitCameraMaxReopenAttemptsReached(cameraOpenSemaphore)

        // Allow camera opening to succeed
        cameraManagerImpl.shouldFailCameraOpen = false

        // Make camera available
        camera2CameraImpl!!.cameraAvailability.onCameraAvailable(cameraId!!)

        // Verify the camera opens
        awaitCameraOpen()
    }

    @Test
    @Throws(Exception::class)
    fun activeResuming_openCameraAfterMultipleReopening_whenCameraUnavailable() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        cameraManagerImpl.shouldEmitCameraInUseError = true
        // Enable active reopening which will open the camera even when camera is unavailable.
        camera2CameraImpl!!.setActiveResumingMode(true)
        // Try opening the camera. This will fail and trigger reopening attempts
        camera2CameraImpl!!.open()
        // make camera unavailable.
        cameraExecutor!!.execute {
            camera2CameraImpl!!.cameraAvailability.onCameraUnavailable(
                cameraId!!
            )
        }
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Wait for half the max reopen attempts to occur
        val maxReopenAttempts = ceil(10000.0 / ACTIVE_REOPEN_DELAY_BASE_MS).toInt()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                maxReopenAttempts / 2, 10000,
                TimeUnit.MILLISECONDS
            )
        ).isTrue()

        // Allow camera opening to succeed
        cameraManagerImpl.shouldFailCameraOpen = false

        // Verify the camera opens
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        awaitCameraOpen()
    }

    private fun openAnotherCamera() {
        val cameraManager = ApplicationProvider.getApplicationContext<Context>()
            .getSystemService(Context.CAMERA_SERVICE) as CameraManager
        anotherCameraDevice =
            AsyncCameraDevice(cameraManager, cameraId!!, cameraHandler!!).apply { openAsync() }
    }

    @SdkSuppress(minSdkVersion = 23)
    @Test
    @Throws(Exception::class)
    fun activeResuming_reopenCamera_whenCameraIsInterruptedInActiveResuming() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Open camera will succeed.
        cameraManagerImpl.shouldFailCameraOpen = false
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        awaitCameraOpen()

        // Enable active resuming which will open the camera even when camera is unavailable.
        camera2CameraImpl!!.setActiveResumingMode(true)

        // Disconnect the current camera.
        openAnotherCamera()

        // Verify the camera opens
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        awaitCameraOpen()
    }

    @SdkSuppress(minSdkVersion = 23)
    @Test
    @Throws(Exception::class)
    fun activeResuming_reopenCamera_whenCameraPendingOpenThenResume() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener = object :
            FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
            override fun onCameraOpenAttempt() {
                cameraOpenSemaphore.release()
            }
        }

        // Open camera will succeed.
        cameraManagerImpl.shouldFailCameraOpen = false
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        awaitCameraOpen()

        // Disconnect the current camera.
        openAnotherCamera()

        // Wait until camera is pending_open which will wait for camera availability
        awaitCameraPendingOpen()
        // Enable active resuming to see if it can resume the camera.
        camera2CameraImpl!!.setActiveResumingMode(true)

        // Verify the camera opens
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        awaitCameraOpen()
    }

    @Test
    @Throws(Exception::class)
    fun activeResuming_doNotReopenCamera_whenCameraDeviceError() {
        // Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl()
        setUpCamera(cameraManagerImpl)

        // Set up camera open attempt listener
        val cameraOpenSemaphore = Semaphore(0)
        cameraManagerImpl.onCameraOpenAttemptListener =
            object : FailCameraOpenCameraManagerImpl.OnCameraOpenAttemptListener {
                override fun onCameraOpenAttempt() {
                    cameraOpenSemaphore.release()
                }
            }

        // Open camera will succeed.
        cameraManagerImpl.shouldFailCameraOpen = false
        camera2CameraImpl!!.open()
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isTrue()
        // wait onOpened to get CameraDevice
        awaitCameraOpen()
        camera2CameraImpl!!.setActiveResumingMode(true)
        cameraExecutor!!.execute {
            cameraManagerImpl.deviceStateCallback!!.notifyOnError(
                CameraDevice.StateCallback.ERROR_CAMERA_DEVICE
            )
        }

        // Make camera unavailable after onClosed so that we can verify if camera is not opened
        // again. For ERROR_CAMERA_DEVICE, it should not reopen the camera if the camera is
        // not available.
        cameraManagerImpl.deviceStateCallback!!.runWhenOnClosed { openAnotherCamera() }

        // 2nd camera open should not happen
        Truth.assertThat(
            cameraOpenSemaphore.tryAcquire(
                1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(),
                TimeUnit.MILLISECONDS
            )
        ).isFalse()
    }

    @Test
    fun openCameraExceptionWithoutOnError_successReopenCamera() {
        val errorTimeout = 2000L

        // Arrange. Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
            shouldEmitExceptionWithoutOnError = true
        }
        setUpCamera(cameraManagerImpl)

        val cameraOpenStateSemaphore = Semaphore(0)
        val stateObserver = object : Observable.Observer<CameraInternal.State?> {
            override fun onNewData(newState: CameraInternal.State?) {
                if (newState == CameraInternal.State.OPEN) {
                    cameraOpenStateSemaphore.release()
                }
            }

            override fun onError(t: Throwable) {
                Logger.e("CameraReopenTest", "Camera state error: " + t.message)
            }
        }
        camera2CameraImpl!!.cameraState.addObserver(
            CameraXExecutors.directExecutor(), stateObserver
        )
        // Act. Try opening the camera. This will fail and trigger reopening.
        camera2CameraImpl!!.open()

        // Assume the openCamera() should be called.
        assumeThat(
            "The test should not able to open camera directly",
            cameraOpenStateSemaphore.tryAcquire(errorTimeout, TimeUnit.MILLISECONDS),
            equalTo(false)
        )
        camera2CameraImpl!!.cameraState.removeObserver(stateObserver)

        // Allow camera opening to succeed
        cameraManagerImpl.apply {
            shouldFailCameraOpen = false
            shouldEmitExceptionWithoutOnError = false
        }
        // Assert. Verify the camera opens
        awaitCameraState(
            CameraInternal.State.OPEN,
            errorTimeout + REOPEN_DELAY_MS + WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS
        )
    }

    @Test
    fun openCameraExceptionWithoutOnError_cameraReleaseBeforeReopen() {
        // Arrange. Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
            shouldEmitExceptionWithoutOnError = true
        }
        setUpCamera(cameraManagerImpl)

        // Try opening the camera. This will fail.
        camera2CameraImpl!!.open()
        awaitCameraState(CameraInternal.State.OPENING, 1000)

        // Act. release camera2CameraImpl
        camera2CameraImpl!!.release()

        // Assert. Verify the camera switches to the RELEASED state.
        awaitCameraState(CameraInternal.State.RELEASED, 1000)
    }

    @Test
    fun openCameraExceptionWithoutOnError_cameraCloseBeforeReopen() {
        // Arrange. Set up the camera
        val cameraManagerImpl = FailCameraOpenCameraManagerImpl().apply {
            shouldEmitExceptionWithoutOnError = true
        }
        setUpCamera(cameraManagerImpl)

        // Try opening the camera. This will fail.
        camera2CameraImpl!!.open()
        awaitCameraState(CameraInternal.State.OPENING, 1000)

        // Act. close camera2CameraImpl
        camera2CameraImpl!!.close()

        // Assert. Verify the camera switches to the CLOSED state.
        awaitCameraState(CameraInternal.State.CLOSED, 1000)
    }

    @Throws(CameraAccessExceptionCompat::class, CameraUnavailableException::class)
    private fun setUpCamera(cameraManagerImpl: FailCameraOpenCameraManagerImpl) {
        // Build camera manager wrapper
        val cameraManagerCompat = CameraManagerCompat.from(cameraManagerImpl)

        // Build camera info
        val camera2CameraInfo = Camera2CameraInfoImpl(
            cameraId!!, cameraManagerCompat
        )

        // Initialize camera instance
        camera2CameraImpl = Camera2CameraImpl(
            ApplicationProvider.getApplicationContext(),
            cameraManagerCompat,
            cameraId!!,
            camera2CameraInfo,
            cameraCoordinator,
            CameraStateRegistry(cameraCoordinator, 1),
            cameraExecutor!!,
            cameraHandler!!,
            DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext()),
            -1L
        )
    }

    @Throws(InterruptedException::class)
    private fun awaitCameraOpen() {
        awaitCameraState(CameraInternal.State.OPEN)
    }

    @Throws(InterruptedException::class)
    private fun awaitCameraPendingOpen() {
        awaitCameraState(CameraInternal.State.PENDING_OPEN)
    }

    private fun awaitCameraState(
        state: CameraInternal.State,
        timeoutMs: Long = WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong()
    ) {
        val cameraStateSemaphore = Semaphore(0)
        val observer: Observable.Observer<CameraInternal.State?> =
            object : Observable.Observer<CameraInternal.State?> {
                override fun onNewData(newState: CameraInternal.State?) {
                    if (newState == state) {
                        cameraStateSemaphore.release()
                    }
                }

                override fun onError(t: Throwable) {
                    Logger.e("CameraReopenTest", "Camera state error: " + t.message)
                }
            }
        camera2CameraImpl!!.cameraState.addObserver(
            CameraXExecutors.directExecutor(),
            observer
        )
        try {
            Truth.assertThat(
                cameraStateSemaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)
            ).isTrue()
        } finally {
            camera2CameraImpl!!.cameraState.removeObserver(observer)
        }
    }

    @Throws(InterruptedException::class)
    private fun awaitCameraMaxReopenAttemptsReached(cameraOpenSemaphore: Semaphore) {
        while (true) {
            val cameraOpenAttempted = cameraOpenSemaphore.tryAcquire(
                1,
                WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS
            )
            if (!cameraOpenAttempted) {
                return
            }
        }
    }

    companion object {
        private const val REOPEN_DELAY_MS =
            Camera2CameraImpl.StateCallback.CameraReopenMonitor.REOPEN_DELAY_MS
        private const val REOPEN_LIMIT_MS =
            Camera2CameraImpl.StateCallback.CameraReopenMonitor.REOPEN_LIMIT_MS
        private const val ACTIVE_REOPEN_DELAY_BASE_MS =
            Camera2CameraImpl.StateCallback.CameraReopenMonitor.ACTIVE_REOPEN_DELAY_BASE_MS

        private const val WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS = 2000
        private var cameraHandlerThread: HandlerThread? = null
        private var cameraHandler: Handler? = null
        private var cameraExecutor: ExecutorService? = null

        @BeforeClass
        @JvmStatic
        fun classSetup() {
            cameraHandlerThread = HandlerThread("cameraThread")
            cameraHandlerThread!!.start()
            cameraHandler = HandlerCompat.createAsync(
                cameraHandlerThread!!.looper
            )
            cameraExecutor = CameraXExecutors.newHandlerExecutor(
                cameraHandler!!
            )
        }

        @AfterClass
        @JvmStatic
        fun classTeardown() {
            cameraHandlerThread!!.quitSafely()
        }
    }
}

/**
 * Wraps a [CameraManagerCompat.CameraManagerCompatImpl] instance and controls camera opening
 * by either allowing it to succeed or fail.
 */
@RequiresApi(21)
internal class FailCameraOpenCameraManagerImpl : CameraManagerCompat.CameraManagerCompatImpl {
    private val forwardCameraManagerCompatImpl:
        CameraManagerCompat.CameraManagerCompatImpl by lazy {
        CameraManagerCompat.CameraManagerCompatImpl.from(
            ApplicationProvider.getApplicationContext(),
            MainThreadAsyncHandler.getInstance()
        )
    }
    private val lock = Any()

    @GuardedBy("lock")
    var onCameraOpenAttemptListener: OnCameraOpenAttemptListener? = null
        get() {
            synchronized(lock) {
                return field
            }
        }
        set(value) {
            synchronized(lock) {
                field = value
            }
        }

    @GuardedBy("lock")
    var shouldFailCameraOpen = true
        get() {
            synchronized(lock) {
                return field
            }
        }
        set(value) {
            synchronized(lock) {
                field = value
            }
        }
    @Volatile
    var shouldEmitCameraInUseError = false

    @Volatile
    var shouldEmitExceptionWithoutOnError = false

    @GuardedBy("lock")
    var deviceStateCallback: CameraDeviceCallbackWrapper? = null
        get() {
            synchronized(lock) {
                return field
            }
        }
        set(value) {
            synchronized(lock) {
                field = value
            }
        }

    @Throws(CameraAccessExceptionCompat::class)
    override fun getCameraIdList(): Array<String> {
        return forwardCameraManagerCompatImpl.cameraIdList
    }

    @Throws(CameraAccessExceptionCompat::class)
    override fun getConcurrentCameraIds(): MutableSet<MutableSet<String>> {
        return forwardCameraManagerCompatImpl.concurrentCameraIds
    }

    override fun registerAvailabilityCallback(
        executor: Executor,
        callback: AvailabilityCallback
    ) {
        forwardCameraManagerCompatImpl.registerAvailabilityCallback(executor, callback)
    }

    override fun unregisterAvailabilityCallback(
        callback: AvailabilityCallback
    ) {
        forwardCameraManagerCompatImpl.unregisterAvailabilityCallback(callback)
    }

    @Throws(CameraAccessExceptionCompat::class)
    override fun getCameraCharacteristics(camId: String): CameraCharacteristics {
        return forwardCameraManagerCompatImpl.getCameraCharacteristics(camId)
    }

    override fun getCameraManager(): CameraManager {
        return forwardCameraManagerCompatImpl.cameraManager
    }

    @Throws(CameraAccessExceptionCompat::class)
    override fun openCamera(
        camId: String,
        executor: Executor,
        callback: CameraDevice.StateCallback
    ) {
        synchronized(lock) {
            deviceStateCallback = CameraDeviceCallbackWrapper(callback)
            if (onCameraOpenAttemptListener != null) {
                onCameraOpenAttemptListener!!.onCameraOpenAttempt()
            }
            if (shouldFailCameraOpen) {
                if (shouldEmitExceptionWithoutOnError) {
                    throw CameraAccessExceptionCompat(2)
                } else if (shouldEmitCameraInUseError) {
                    executor.execute {
                        callback.onError(
                            mock(CameraDevice::class.java),
                            CameraDevice.StateCallback.ERROR_CAMERA_IN_USE
                        )
                    }
                }
                // Throw any exception
                throw SecurityException("Lacking privileges")
            } else {
                forwardCameraManagerCompatImpl
                    .openCamera(camId, executor, deviceStateCallback!!)
            }
        }
    }

    interface OnCameraOpenAttemptListener {
        /** Triggered whenever an attempt to open the camera is made.  */
        fun onCameraOpenAttempt()
    }

    internal class CameraDeviceCallbackWrapper(
        private val deviceCallback: CameraDevice.StateCallback
    ) : CameraDevice.StateCallback() {
        private var cameraDevice: CameraDevice? = null
        private var runnableForOnClosed: Runnable? = null
        override fun onOpened(device: CameraDevice) {
            cameraDevice = device
            deviceCallback.onOpened(device)
        }

        override fun onClosed(camera: CameraDevice) {
            deviceCallback.onClosed(camera)
            if (runnableForOnClosed != null) {
                runnableForOnClosed!!.run()
            }
        }

        override fun onDisconnected(device: CameraDevice) {
            deviceCallback.onDisconnected(device)
        }

        override fun onError(device: CameraDevice, error: Int) {
            deviceCallback.onError(device, error)
        }

        fun notifyOnError(error: Int) {
            onError(cameraDevice!!, error)
        }

        fun runWhenOnClosed(runnable: Runnable) {
            runnableForOnClosed = runnable
        }
    }
}
